﻿///  Copyright (c) 2010 Ray Liang (http://www.dotnetage.com)
///  Dual licensed under the MIT and GPL licenses:
///  http://www.opensource.org/licenses/mit-license.php
///  http://www.gnu.org/licenses/gpl.html

using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Security.Permissions;
using System.ComponentModel;
using System.Web.UI.WebControls;
using System.ComponentModel.Design;
using System.Drawing.Design;
using System.Web.Script;

namespace DNA.UI.JQuery
{
    /// <summary>
    /// AutoComplete is an  ASP.NET WebControl that encapsulated the jQuery UI autocomplete plugin 
    /// that can be attached to any TextBox control, and will associate that control with a popup panel to display words that begin with the prefix typed 
    /// into the textbox.
    /// </summary>
    [JQuery(Name = "autocomplete", Assembly = "jQuery", DisposeMethod = "destroy",
        ScriptResources = new string[] { "ui.core.js", "ui.widget.js", "ui.position.js", "ui.autocomplete.js" }, StartEvent = ClientRegisterEvents.DocumentReady)]
    [AspNetHostingPermission(SecurityAction.Demand, Level = AspNetHostingPermissionLevel.Minimal)]
    [AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
    [System.Drawing.ToolboxBitmap(typeof(DatePicker), "AutoComplete.AutoComplete.ico")]
    [ToolboxData("<{0}:AutoComplete runat=\"server\" ID=\"AutoComplete1\"></{0}:AutoComplete>")]
    [ParseChildren(true)]
    [Designer(typeof(Design.NoneUIControlDesigner))]
    public class AutoComplete : Control
    {
        private JQuerySelector target = new JQuerySelector();
        private string[] source;

        /// <summary>
        /// Gets/Sets the which control plugin apply to. 
        /// </summary>
        [Bindable(true)]
        [TypeConverter(typeof(ExpandableObjectConverter))]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [NotifyParentProperty(true)]
        [Category("Action")]
        public JQuerySelector Target
        {
            get
            {
                return target;
            }
            set
            {
                target = value;
            }
        }

        /// <summary>
        /// The delay in milliseconds the Autocomplete waits after a keystroke to activate itself. A zero-delay makes sense for local data (more responsive), but can produce a lot of load for remote data, while being less responsive.
        /// </summary>
        [Description("Gets/Sets the delay in milliseconds the Autocomplete waits after a keystroke to activate itself. A zero-delay makes sense for local data (more responsive), but can produce a lot of load for remote data, while being less responsive.")]
        [Category("Behavior")]
        [JQueryOption("delay", IgnoreValue = 0)]
        public int Delay { get; set; }

        /// <summary>
        /// The minimum number of characters a user has to type before the Autocomplete activates. Zero is useful for local data with just a few items. Should be increased when there are a lot of items, where a single character would match a few thousand items.
        /// </summary>
        [Description("Gets/Sets the minimum number of characters a user has to type before the Autocomplete activates. Zero is useful for local data with just a few items. Should be increased when there are a lot of items, where a single character would match a few thousand items.")]
        [Category("Behavior")]
        [JQueryOption("minLength", IgnoreValue = 0)]
        public int MinLength { get; set; }

        /// <summary>
        /// Gets/Sets whether enabled cache the remote data result.
        /// </summary>
        [Description("Gets/Sets whether enabled cache the remote data result.")]
        [Category("Behavior")]
        public bool EnabledCache { get; set; }

        /// <summary>
        /// Gets/Sets whether enabled AutoComplete select multiple values."
        /// </summary>
        [Description("Gets/Sets whether enabled AutoComplete select multiple values.")]
        [Category("Behavior")]
        public bool IsMultipleValueSupports { get; set; }

        [Category("Data")]
        [Description("Gets the colleciton data")]
        [PersistenceMode(PersistenceMode.Attribute)]
        [NotifyParentProperty(true)]
        [TypeConverter(typeof(StringArrayConverter))]
        public string[] Source
        {
            get { return source; }
            set { source = value; }
        }

        /// <summary>
        ///  Number of suggestions to be retrieved from the web service.
        /// </summary>
        [Description("Gets/Sets the number of suggestions to be retrieved from the web service.")]
        [Category("Behavior")]
        public int CompletionSetCount { get; set; }

        /// <summary>
        /// Gets/Sets the web service method to be called
        /// </summary>
        [Description("Gets/Sets the web service method to be called")]
        [Category("Data")]
        public string ServiceMethod { get; set; }

        /// <summary>
        /// Gets/Sets the path to the web service that the extender will pull the word or sentence completions from. If this is not provided, the service method should be a page method.
        /// </summary>
        [Description("Gets/Sets the path to the web service that the extender will pull the word or sentence completions from. If this is not provided, the service method should be a page method.")]
        [Category("Data")]
        public string ServicePath { get; set; }

        #region Client events

        /// <summary>
        /// Triggered when an item is selected from the menu; ui.item refers to the selected item. The default action of select is to replace the text field's value with the value of the selected item. Canceling this event prevents the value from being updated, but does not prevent the menu from closing.
        /// </summary>
        [Description("Triggered when an item is selected from the menu; ui.item refers to the selected item. The default action of select is to replace the text field's value with the value of the selected item. Canceling this event prevents the value from being updated, but does not prevent the menu from closing.")]
        [Category("ClientEvents")]
        [Bindable(true)]
        [PersistenceMode(PersistenceMode.Attribute)]
        [TypeConverter(typeof(MultilineStringConverter))]
        [Editor(typeof(MultilineStringEditor), typeof(UITypeEditor))]
        [JQueryOption(Name = "select", Type = JQueryOptionTypes.Function, FunctionParams = new string[] { "event", "ui" })]
        public string OnClientItemSelect { get; set; }

        /// <summary>
        /// Before a request (source-option) is started, after minLength and delay are met. Can be canceled (return false), then no request will be started and no items suggested.
        /// </summary>
        [Description("Before a request (source-option) is started, after minLength and delay are met. Can be canceled (return false), then no request will be started and no items suggested.")]
        [Category("ClientEvents")]
        [Bindable(true)]
        [PersistenceMode(PersistenceMode.Attribute)]
        [TypeConverter(typeof(MultilineStringConverter))]
        [Editor(typeof(MultilineStringEditor), typeof(UITypeEditor))]
        [JQueryOption(Name = "search", Type = JQueryOptionTypes.Function, FunctionParams = new string[] { "event", "ui" })]
        public string OnClientItemSearch { get; set; }

        /// <summary>
        /// Triggered when the suggestion menu is opened.
        /// </summary>
        [Description("Triggered when the suggestion menu is opened.")]
        [Category("ClientEvents")]
        [Bindable(true)]
        [PersistenceMode(PersistenceMode.Attribute)]
        [TypeConverter(typeof(MultilineStringConverter))]
        [Editor(typeof(MultilineStringEditor), typeof(UITypeEditor))]
        [JQueryOption(Name = "open", Type = JQueryOptionTypes.Function, FunctionParams = new string[] { "event", "ui" })]
        public string OnClientOpen { get; set; }

        /// <summary>
        /// When the list is hidden - doesn't have to occur together with a change.
        /// </summary>
        [Description("When the list is hidden - doesn't have to occur together with a change.")]
        [Category("ClientEvents")]
        [Bindable(true)]
        [PersistenceMode(PersistenceMode.Attribute)]
        [TypeConverter(typeof(MultilineStringConverter))]
        [Editor(typeof(MultilineStringEditor), typeof(UITypeEditor))]
        [JQueryOption(Name = "close", Type = JQueryOptionTypes.Function, FunctionParams = new string[] { "event", "ui" })]
        public string OnClientClose { get; set; }

        /// <summary>
        /// After an item was selected; ui.item refers to the selected item. Always triggered after the close event.
        /// </summary>
        [Description("After an item was selected; ui.item refers to the selected item. Always triggered after the close event.")]
        [Category("ClientEvents")]
        [Bindable(true)]
        [PersistenceMode(PersistenceMode.Attribute)]
        [TypeConverter(typeof(MultilineStringConverter))]
        [Editor(typeof(MultilineStringEditor), typeof(UITypeEditor))]
        [JQueryOption(Name = "change", Type = JQueryOptionTypes.Function, FunctionParams = new string[] { "event", "ui" })]
        public string OnClientChange { get; set; }

        /// <summary>
        /// Before focus is moved to an item (not selecting), ui.item refers to the focused item. The default action of focus is to replace the text field's value with the value of the focused item, though only if the focus event was triggered by a keyboard interaction. Canceling this event prevents the value from being updated, but does not prevent the menu item from being focused.
        /// </summary>
        [Description("Before focus is moved to an item (not selecting), ui.item refers to the focused item. The default action of focus is to replace the text field's value with the value of the focused item, though only if the focus event was triggered by a keyboard interaction. Canceling this event prevents the value from being updated, but does not prevent the menu item from being focused.")]
        [Category("ClientEvents")]
        [Bindable(true)]
        [PersistenceMode(PersistenceMode.Attribute)]
        [TypeConverter(typeof(MultilineStringConverter))]
        [Editor(typeof(MultilineStringEditor), typeof(UITypeEditor))]
        [JQueryOption(Name = "focus", Type = JQueryOptionTypes.Function, FunctionParams = new string[] { "event", "ui" })]
        public string OnClientInputFocus { get; set; }

        #endregion

        protected override void OnPreRender(EventArgs e)
        {

            JQueryScriptBuilder builder = new JQueryScriptBuilder(this, Target);
            if (EnabledCache)
                builder.Scripts.Append("window." + this.ClientID + "_cache={};");
            string splitDelegate = "var _split=function(val){return val.split( /,\\s*/ );};";
            string extractDelegate = "var _extractLast=function(term){return _split(term).pop();};";

            if (Source != null)
            {
                if (IsMultipleValueSupports)
                {
                    string[] formatted = new string[Source.Length];
                    for (int i = 0; i < Source.Length; i++)
                        formatted[i] = "'" + Source[i] + "'";
                    string _args = "var _availableTags=[" + string.Join(",", formatted) + "];";
                    var _multiScripts = new StringBuilder();
                    _multiScripts.Append(splitDelegate)
                        .AppendLine(extractDelegate)
                        .AppendLine(_args)
                        .AppendLine("response( $.ui.autocomplete.filter(")
                        .AppendLine("_availableTags, _extractLast(request.term)));");

                    builder.AddFunctionOption("source", _multiScripts.ToString(), new string[] { "request", "response" });
                    this.OnClientInputFocus = "return false;";
                    this.OnClientItemSelect = "var terms= this.value.split(/,\\s*/ );terms.pop();terms.push(ui.item.value);terms.push(\"\" );this.value = terms.join(',');return false;";
                }
                else
                    builder.AddOption("source", Source);
            }
            else
            {
                if ((!string.IsNullOrEmpty(ServicePath)) && (!string.IsNullOrEmpty(ServiceMethod)))
                {
                    var path = ServicePath + "/" + ServiceMethod;
                    if (path.StartsWith("~"))
                        path = VirtualPathUtility.ToAbsolute(path);

                    var scripts = new StringBuilder();
                    scripts.Append("var _term=request.term;");

                    if (IsMultipleValueSupports)
                    {
                        scripts.AppendLine(splitDelegate)
                            .AppendLine(extractDelegate)
                            .AppendLine("_term=_extractLast(_term);");

                        this.OnClientItemSearch =splitDelegate+ extractDelegate+"var term = _extractLast( this.value );if ( term.length < 2 ) {return false;}";
                        this.OnClientInputFocus = "return false;";
                        this.OnClientItemSelect = "var terms= this.value.split(/,\\s*/ );terms.pop();terms.push(ui.item.value);terms.push(\"\" );this.value = terms.join(',');return false;";
                    }

                    scripts.Append("$.ajax({")
                             .Append("type:'POST'")
                             .Append(",data: \"{'prefix':'\"+_term+\"','count':'" + CompletionSetCount.ToString() + "'}\"")
                             .Append(",url:'")
                             .Append(path)
                             .Append("',dataType:'json'")
                             .Append(",contentType:'application/json'")
                             .Append(",success:function(data){");

                    if (EnabledCache)
                        scripts.Append("window." + this.ClientID + "_cache[term]=data.d;");

                    scripts.Append("response(data.d);")
                              .Append("}})");


                    if (EnabledCache)
                    {
                        var cacheScripts = new StringBuilder();
                        cacheScripts.Append("var term=request.term;")
                            .Append(" if (term in window." + this.ClientID + "_cache) {")
                            .Append("response(window." + this.ClientID + "_cache[term]);")
                            .Append("return ; }");
                        scripts.Insert(0, cacheScripts.ToString());
                    }

                    builder.AddFunctionOption("source", scripts.ToString(), new string[] { "request", "response" });
                }
            }

            //ClientScriptManager.RegisterDocumentReadyScript(this,"window."+this.ClientID+"_cache={};");
            ClientScriptManager.RegisterJQueryControl(this, builder);
        }

    }
}
